﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;
using Odyssey.Controls.Primitives;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace Odyssey.Controls
{
    [TemplatePart(Name = partItemsPanel, Type = typeof(InfiniteLoopPanel))]
    public class ItemPicker : Control, IInfiniteLoopHost, ICollectionChangedListener
    {
        private const string partItemsPanel = "PART_ItemsPanel";

        public ItemPicker()
            : base()
        {
            this.DefaultStyleKey = typeof(ItemPicker);
            yOffsetTransform = new TranslateTransform();
            Loaded += (s, e) => IsLoaded = true;
            Unloaded += (s, e) => IsLoaded = false;
        }

        private InfiniteLoopPanel itemsPanel;
        private TranslateTransform yOffsetTransform;
        private Storyboard yOffsetStoryboard;
        private bool isExpanded;
        private WeakCollectionChangedListener collectionChangedListener;
        private Dictionary<int, ItemPickerItem> materializedItems = new Dictionary<int, ItemPickerItem>();
        private Queue<ItemPickerItem> recycledItems = new Queue<ItemPickerItem>();

        protected bool IsInfinite
        {
            get { return itemsPanel != null ? itemsPanel.IsInfinite : false; }
        }

        /// <summary>
        /// Gets whether the control is loaded.
        /// </summary>
        protected bool IsLoaded;

        /// <summary>
        /// Gets or sets the vertical offset relative to the first item.
        /// </summary>
        protected double VerticalOffset
        {
            get { return yOffsetTransform != null ? yOffsetTransform.Y : 0.0; }
            set
            {
                if (yOffsetTransform != null)
                {
                    if (!IsInfinite)
                    {
                        if (value > 0.0) value = 0.0;
                        else if (-value > ItemHeight * (ItemCount - 1)) value = -ItemHeight * (ItemCount - 1);
                    }
                    yOffsetTransform.Y = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the selected item.
        /// </summary>
        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(ItemPicker), new PropertyMetadata(null, OnSelectedItemChanged));

        private static void OnSelectedItemChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            (o as ItemPicker).OnSelectedItemChanged(args.OldValue, args.NewValue);
        }

        /// <summary>
        /// Gets the index of the selected item.
        /// </summary>
        public int SelectedIndex
        {
            get
            {
                object selectedItem = SelectedItem;
                if (selectedItem == null) return -1;
                var source = ItemsSource;
                if (source == null) return -1;
                return source.IndexOf(selectedItem);
            }
        }

        protected virtual void OnSelectedItemChanged(object oldValue, object newValue)
        {
            StopOffsetAnimation();
            if (oldValue != null)
            {
                var container = GetContainerFromItem(oldValue);
                if (container != null)
                {
                    container.IsSelected = false;
                }
            }
            if (newValue != null)
            {
                var container = GetContainerFromItem(newValue);
                if (container != null)
                {
                    container.IsSelected = true;
                }

                BalanceSelectedItem();

            }
            OnSelectionChanged();

        }

        /// <summary>
        /// Gets the number of item in ItemsSource, otherwise -1.
        /// </summary>
        protected int ItemCount
        {
            get
            {
                var source = ItemsSource;
                return source != null ? source.Count : 0;
            }
        }

        /// <summary>
        /// Gets or sets the source for the items.
        /// </summary>
        public IList ItemsSource
        {
            get { return (IList)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IList), typeof(ItemPicker), new PropertyMetadata(null, OnItemsSourceChanged));

        private static void OnItemsSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            (o as ItemPicker).OnItemsSourceChanged((IList)args.OldValue, (IList)args.NewValue);
        }

        private void OnItemsSourceChanged(IList oldValue, IList newValue)
        {
            if (collectionChangedListener != null)
            {
                collectionChangedListener.Disconnect();
                collectionChangedListener = null;
            }
            INotifyCollectionChanged newCol = newValue as INotifyCollectionChanged;
            if (newCol != null)
            {
                collectionChangedListener = WeakCollectionChangedListener.CreateIfNecessary(newCol, this);
            }
        }


        /// <summary>
        /// Gets or sets the item template.
        /// </summary>
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ItemPicker), new PropertyMetadata(null));


        /// <summary>
        /// Gets or sets the height of each item.
        /// </summary>
        public double ItemHeight
        {
            get { return (double)GetValue(ItemHeightProperty); }
            set { SetValue(ItemHeightProperty, value); }
        }

        public static readonly DependencyProperty ItemHeightProperty =
            DependencyProperty.Register("ItemHeight", typeof(double), typeof(ItemPicker), new PropertyMetadata(146.0));


        /// <summary>
        /// Gets whether the control is in a flick animation.
        /// </summary>
        public bool IsScrolling
        {
            get { return (bool)GetValue(IsScrollingProperty); }
            private set { SetValue(IsScrollingProperty, value); }
        }

        public static readonly DependencyProperty IsScrollingProperty =
            DependencyProperty.Register("IsScrolling", typeof(bool), typeof(ItemPicker), new PropertyMetadata(false));


        /// <summary>
        /// Gets whether the control is expanded.
        /// </summary>
        public bool IsExpanded
        {
            get { return (bool)GetValue(IsExpandedProperty); }
            internal set { SetValue(IsExpandedProperty, value); }
        }

        public static readonly DependencyProperty IsExpandedProperty =
            DependencyProperty.Register("IsExpanded", typeof(bool), typeof(ItemPicker), new PropertyMetadata(false, OnExpansionChanged));

        private static void OnExpansionChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            bool value = (bool)args.NewValue;
            (o as ItemPicker).OnExpansionChanged(!value, value);
        }

        protected virtual void OnExpansionChanged(bool oldValue, bool newValue)
        {
            foreach (var container in materializedItems.Values)
            {
                container.IsExpanded = newValue || container.IsSelected;
            }

            if (!isExpanded && newValue)
            {
                isExpanded = true;
            //    itemsPanel.Refresh();
            }

            if (itemsPanel != null) itemsPanel.Refresh();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            itemsPanel = GetTemplateChild(partItemsPanel) as InfiniteLoopPanel;
            if (itemsPanel == null) throw new ArgumentNullException(partItemsPanel, "TemplatePart not found in Template.");
            itemsPanel.RenderTransform = yOffsetTransform;
            itemsPanel.ItemsHost = this;
            // itemsPanel.LayoutUpdated += new EventHandler(itemsPanel_LayoutUpdated);
        }

        void itemsPanel_LayoutUpdated(object sender, EventArgs e)
        {
            itemsPanel.LayoutUpdated -= itemsPanel_LayoutUpdated;
            itemsPanel.Refresh();
        }

        protected virtual bool IsItemItsOwnContainerOverride(object item)
        {
            return item is ItemPickerItem;
        }

        protected virtual DependencyObject GetContainerForItemOverride()
        {
            ItemPickerItem item = new ItemPickerItem();
            item.Height = ItemHeight;
            return item;
        }

        protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
        {
            if (!e.Handled)
            {
                IsExpanded = true;
                SelectedItem = null;
                double delta = e.DeltaManipulation.Translation.Y;
                VerticalOffset += delta;
                e.Handled = true;
            }
            base.OnManipulationDelta(e);
        }

        protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
        {
            Focus();
            StopOffsetAnimation();
            e.Handled = true;
            base.OnManipulationStarted(e);
        }

        protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
        {
            const double velocityThreshold = 0.01;
            double velocity = e.FinalVelocities.LinearVelocity.Y;
            if (Math.Abs(velocity) > velocityThreshold)
            {
                AnimateByVeloctity(velocity);
            }
            else
            {
                DetermineSelectedItem();
            }
            e.Handled = true;
            base.OnManipulationCompleted(e);
        }

        private void DetermineSelectedItem()
        {
            var source = ItemsSource;
            if (source == null) return;
            double itemHeight = ItemHeight;
            double y = -VerticalOffset;
            int index = (int)Math.Round(y / itemHeight);
            int count = ItemCount;
            index %= count;
            if (index < 0) index += count;
            SelectedItem = source[index];
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            //StopOffsetAnimation();
            DetermineSelectedItem();
            IsExpanded = false;
            base.OnLostFocus(e);
        }

        internal void BalanceSelectedItem(bool animated = true)
        {
            int count = ItemsSource != null ? ItemsSource.Count : 0;
            double currentOffset, targetOffset;
            GetBalanceOffsets(out currentOffset, out targetOffset);

            if (animated && IsLoaded)
            {
                yOffsetStoryboard = GetBalanceStoryboard(currentOffset, targetOffset);
                if (yOffsetStoryboard != null) yOffsetStoryboard.Begin();
            }
            else
            {
                VerticalOffset = -targetOffset;
            }
        }

        private void GetBalanceOffsets(out double currentOffset, out double targetOffset)
        {
            double itemHeight = ItemHeight;
            double max = itemHeight * ItemCount;
            currentOffset = -VerticalOffset % max;
            targetOffset = SelectedIndex * itemHeight;
            if (currentOffset < 0) currentOffset += max;

            if (IsInfinite)
            {
                if ((targetOffset - currentOffset) > max / 2)
                {
                    targetOffset -= max;
                }
                else if ((currentOffset - targetOffset) > max / 2)
                {
                    targetOffset += max;
                }
            }
        }

        private Storyboard GetBalanceStoryboard(double currentOffset, double targetOffset)
        {
            if (currentOffset == targetOffset) return null;

            Storyboard sb = new Storyboard();
            DoubleAnimation timeline = new DoubleAnimation
            {
                From = -currentOffset,
                To = -targetOffset,
                Duration = TimeSpan.FromSeconds(0.6),
                EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseOut }
            };
            Storyboard.SetTargetProperty(timeline, new PropertyPath("Y"));
            Storyboard.SetTarget(sb, yOffsetTransform);
            sb.Children.Add(timeline);
            return sb;
        }

        private void StopOffsetAnimation()
        {
            double y = VerticalOffset;
            if (yOffsetStoryboard != null)
            {
                if (yOffsetStoryboard.GetCurrentState() == ClockState.Active)
                {
                    yOffsetStoryboard.Stop();
                }
            }
            int count = ItemsSource != null ? ItemsSource.Count : 0;
            VerticalOffset = y % (count * ItemHeight);
            IsScrolling = false;
        }

        private Storyboard GetFlickStoryboard(double deltaOffset, double durationInSeconds)
        {
            Storyboard sb = new Storyboard();
            DoubleAnimation timeline = new DoubleAnimation
            {
                From = VerticalOffset,
                To = VerticalOffset + deltaOffset,
                Duration = TimeSpan.FromSeconds(durationInSeconds),
                //EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
                EasingFunction = PhysicsConstants.GetEasingFunction(durationInSeconds)
            };
            Storyboard.SetTargetProperty(timeline, new PropertyPath("Y"));
            Storyboard.SetTarget(sb, yOffsetTransform);
            sb.Children.Add(timeline);
            sb.Completed += (s, e) => DetermineSelectedItem();
            return sb;
        }

        private void AnimateByVeloctity(double velocity)
        {

            Point point = new Point(0.0, velocity);
            double stopTime = PhysicsConstants.GetStopTime(point);
            Point stopPoint = PhysicsConstants.GetStopPoint(point);

            double delta = stopPoint.Y;
            if (!IsInfinite)
            {
                double stopOffset = delta + VerticalOffset;
                if (stopOffset > 0.0) stopOffset = 0.0;
                else if (-stopOffset > ItemHeight * (ItemCount - 1)) stopOffset = -ItemHeight * (ItemCount - 1);
                delta = stopOffset - VerticalOffset;
                stopTime = 0.25;
            }
            Storyboard sb = GetFlickStoryboard(delta, stopTime);
            this.yOffsetStoryboard = sb;
            sb.Completed += (s, e) => StopOffsetAnimation();
            IsScrolling = true;
            sb.Begin();
        }


        FrameworkElement IInfiniteLoopHost.CreateItem(int index, out bool isNew)
        {
            return GetItemContainer(index, out  isNew);
        }

        void IInfiniteLoopHost.ClearCache()
        {
            foreach (var item in materializedItems.Values)
            {
                recycledItems.Enqueue(item);
            }
            materializedItems.Clear();

        }

        void IInfiniteLoopHost.ReleaseItemContainer(int index)
        {
            this.ReleaseItemContainer(index);
        }

        protected object GetObject(int index)
        {
            IList items = ItemsSource;
            if (index < 0 || items == null || items.Count <= index) throw new ArgumentOutOfRangeException("index");
            return items[index];
        }

        protected ContentControl GetMaterializedItem(int index)
        {
            ItemPickerItem item;
            if (!materializedItems.TryGetValue(index, out item))
            {
                item = null;
            }
            return item;
        }

        protected virtual void ReleaseItemContainer(int index)
        {
            ItemPickerItem item;
            if (materializedItems.TryGetValue(index, out item))
            {
                materializedItems.Remove(index);
                item .IsSelected = false;
                item.IsExpanded = false;
                item.DataContext = null;
                recycledItems.Enqueue(item);
            }
        }


        protected ContentControl GetItemContainer(int index, out bool isNew)
        {
            ItemPickerItem item;
            object obj = GetObject(index);
            bool isValid = isExpanded || object.Equals(obj, SelectedItem);
            if (!isValid)
            {
                isNew = false;
                return null;
            } 

            Queue<ItemPickerItem> recycledItems = this.recycledItems;
            Dictionary<int, ItemPickerItem> materializedItems = this.materializedItems;

            if (materializedItems.TryGetValue(index, out item))
            {
                isNew = false;
            }
            else
            {
                isNew = true;

                bool isSelected = object.Equals(obj, SelectedItem);
                if (recycledItems.Count > 0)
                {
                    item = recycledItems.Dequeue();
                    item.Content = obj;
                    item.DataContext = obj;
                    item.IsExpanded = IsExpanded || isSelected;
                    item.IsSelected = isSelected;
                }
                else
                {

                    DataTemplate template = ItemTemplate;
                    item = new ItemPickerItem
                    {
                        Picker = this,
                        //                        Style = this.ItemContainerStyle,
                        ContentTemplate = template,
                        Content = obj,
                        DataContext = obj,
                        IsSelected = object.Equals(obj, SelectedItem)
                    };
                }
                materializedItems.Add(index, item);
            }
            return item;
        }

        public void PrepareItem(ItemPickerItem item)
        {
            item.IsSelected = object.Equals(item.Content, SelectedItem);
            item.IsExpanded = IsExpanded || item.IsSelected;
        }

        protected ItemPickerItem GetContainerFromItem(object item)
        {
            foreach (var container in materializedItems.Values)
            {
                if (container.Content.Equals(item)) return container;
            }
            return null;
        }

        private ItemPickerItem CreatePickerItem(object data)
        {
            ItemPickerItem item = GetContainerForItemOverride() as ItemPickerItem;
            item.ContentTemplate = ItemTemplate;
            item.Content = data;
            item.IsExpanded = IsExpanded || object.Equals(data, SelectedItem);
            //item.IsSelected = object.Equals(data, SelectedItem);
            item.Picker = this;
            return item;
        }

        protected virtual void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            itemsPanel.OnItemsChanged(e);
            BalanceSelectedItem(false);
        }

        void ICollectionChangedListener.OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnItemsChanged(e);
            ICollectionChangedListener listener = this.itemsPanel as ICollectionChangedListener;
            if (listener != null) listener.OnCollectionChanged(this, e);
        }

        private void OnSelectionChanged()
        {
            var eh = SelectionChanged;
            if (eh != null) eh(this, EventArgs.Empty);
        }

        public event EventHandler SelectionChanged;

    }
}
